<?php
defined('SYS_PATH') or die('No direct script access.');

/**
 * Routes are used to determine the controller and action for a requested URI.
 * Every route generates a regular expression which is used to match a URI
 * and a route. Routes may also contain keys which can be used to set the
 * controller, action, and parameters.
 *
 * Each <key> will be translated to a regular expression using a default
 * regular expression pattern. You can override the default pattern by providing
 * a pattern for the key:
 *
 *     // This route will only match when <id> is a digit
 *     Route::factory('user/edit/<id>', array('id' => '\d+'));
 *
 *     // This route will match when <path> is anything
 *     Route::factory('<path>', array('path' => '.*'));
 *
 * It is also possible to create optional segments by using parentheses in
 * the URI definition:
 *
 *     // This is the standard default route, and no keys are required
 *     Route::default('(<controller>(/<action>(/<id>)))');
 *
 *     // This route only requires the :file key
 *     Route::factory('(<path>/)<file>(<format>)', array('path' => '.*', 'format' => '\.\w+'));
 *
 * Routes also provide a way to generate URIs (called "reverse routing"), which
 * makes them an extremely powerful and flexible way to generate internal links.
 *
 * @package    Ko
 * @author     Ko Team, Eric 
 * @version    $Id: route.php 98 2009-10-09 04:03:35Z wangzh $
 * @copyright  (c) 2008-2009 Ko Team
 * @license    http://kophp.com/license.html
 */
class Route
{

    const REGEX_KEY = '<([a-zA-Z0-9_]++)>';

    const REGEX_SEGMENT = '[^/.,;?]++';

    const REGEX_ESCAPE = '[.\\+*?[^\\]${}=!|]';

    /**
     * @var  string  default action for all routes
     */
    public static $default_action = 'index';

    // List of route objects
    protected static $routes = array();

    // Route URI string
    protected $_uri = '';

    // Regular expressions for route keys
    protected $_regex = array();

    // Default values for route keys
    protected $_defaults = array('action' => 'index');

    // Compiled regex cache
    protected $_route_regex;

    /**
     * Creates a new route. Sets the URI and regular expressions for keys.
     *
     * @param   string   route URI pattern
     * @param   array    key patterns
     * @return  void
     */
    public function __construct ($uri = NULL, array $regex = NULL)
    {
        if ($uri === NULL) {
            // Assume the route is from cache
            return;
        }
        if (! empty($regex)) {
            $this->_regex = $regex;
        }
        // Store the URI that this route will match
        $this->_uri = $uri;
        // Store the compiled regex locally
        $this->_route_regex = $this->_compile();
    }

    /**
     * Called when the object is re-constructed from the cache.
     *
     * @param   array   cached values
     * @return  Route
     */
    public static function __set_state (array $values)
    {
        // Reconstruct the route
        $route = new Route();
        foreach ($values as $key => $value) {
            // Set the route properties
            $route->$key = $value;
        }
        return $route;
    }

    /**
     * Stores a named route and returns it.
     *
     * @param   string   route name
     * @param   string   URI pattern
     * @param   array    regex patterns for route keys
     * @return  Route
     */
    public static function set ($name, $uri, array $regex = NULL)
    {
        return Route::$routes[$name] = new Route($uri, $regex);
    }

    /**
     * Retrieves a named route.
     *
     * @param   string  route name
     * @return  Route
     * @return  FALSE   when no route is found
     */
    public static function get ($name)
    {
        if (! isset(Route::$routes[$name])) {
            throw new KoException('The requested route does not exist: :route', array(':route' => $name));
        }
        return Route::$routes[$name];
    }

    /**
     * Retrieves all named routes.
     *
     * @return  array  named routes
     */
    public static function all ()
    {
        return Route::$routes;
    }

    /**
     * Saves or loads the route cache.
     *
     * @param   boolean   cache the current routes
     * @return  void      when saving routes
     * @return  boolean   when loading routes
     */
    public static function cache ($save = FALSE)
    {
        if ($save === TRUE) {
            Ko::cache('Route::cache()', Route::$routes);
        } else {
            if ($routes = Ko::cache('Route::cache()')) {
                Route::$routes = $routes;
                return TRUE;
            } else {
                return FALSE;
            }
        }
    }

    /**
     * Provides default values for keys when they are not present. The default
     * action will always be "index" unless it is overloaded here.
     *
     *     $route->defaults(array('controller' => 'welcome', 'action' => 'index'));
     *
     * @param   array  key values
     * @return  Route
     */
    public function defaults (array $defaults = NULL)
    {
        $this->_defaults = $defaults;
        return $this;
    }

    /**
     * Tests if the route matches a given URI. A successful match will return
     * all of the routed parameters as an array. A failed match will return
     * boolean FALSE.
     *
     *     // This route will only match if the <controller>, <action>, and <id> exist
     *     $params = Route::factory('<controller>/<action>/<id>', array('id' => '\d+'))
     *         ->matches('users/edit/10');
     *     // The parameters are now: controller = users, action = edit, id = 10
     *
     * This method should almost always be used within an if/else block:
     *
     *     if ($params = $route->matches($uri))
     *     {
     *         // Parse the parameters
     *     }
     *
     * @param   string  URI to match
     * @return  array   on success
     * @return  FALSE   on failure
     */
    public function matches ($uri)
    {
        if (! preg_match($this->_route_regex, $uri, $matches))
            return FALSE;
        $params = array();
        foreach ($matches as $key => $value) {
            if (is_int($key)) {
                continue;
            }
            $params[$key] = $value;
        }
        foreach ($this->_defaults as $key => $value) {
            if (! isset($params[$key]) or $params[$key] === '') {
                $params[$key] = $value;
            }
        }
        // TODO
        if (isset($params['__KO_VARS__'])) {
            $path = explode('/', $params['__KO_VARS__']);
            $count = count($path);
            for ($i = 0; $i < $count; $i += 2) {
                $var = urldecode($path[$i]);
                if (! is_string($value))
                    continue;
                $params[$var] = (isset($path[$i + 1])) ? urldecode($path[$i + 1]) : null;
            }
            unset($params['__KO_VARS__']);      
        }
        return $params;
    }

    /**
     * Generates a URI for the current route based on the parameters given.
     *
     * @param   array   URI parameters
     * @return  string
     */
    public function uri (array $params = NULL)
    {
        if ($params === NULL) {
            // Use the default parameters
            $params = $this->_defaults;
        } else {
            // Add the default parameters
            $params += $this->_defaults;
        }
        // Start with the routed URI
        $uri = $this->_uri;
        if (strpos($uri, '<') === FALSE && strpos($uri, '(') === FALSE) {
            // This is a static route, no need to replace anything
            return $uri;
        }
        while (preg_match('#\([^()]++\)#', $uri, $match)) {
            // Search for the matched value
            $search = $match[0];
            // Remove the parenthesis from the match as the replace
            $replace = substr($match[0], 1, - 1);
            while (preg_match('#' . Route::REGEX_KEY . '#', $replace, $match)) {
                list ($key, $param) = $match;
                if (! empty($params[$param])) {
                    // Replace the key with the parameter value
                    $replace = str_replace($key, $params[$param], $replace);
                } else {
                    // This group has missing parameters
                    $replace = '';
                    break;
                }
            }
            // Replace the group in the URI
            $uri = str_replace($search, $replace, $uri);
        }
        while (preg_match('#' . Route::REGEX_KEY . '#', $uri, $match)) {
            list ($key, $param) = $match;
            if (empty($params[$param])) {
                // Ungrouped parameters are required
                throw new KoException('Required route parameter not passed: :param', array(':param' => $param));
            }
            $uri = str_replace($key, $params[$param], $uri);
        }
        // Trim all extra slashes from the URI
        $uri = preg_replace('#//+#', '/', rtrim($uri, '/'));
        return $uri;
    }

    /**
     * Returns the compiled regular expression for the route. This translates
     * keys and optional groups to a proper PCRE regular expression.
     *
     * @access  protected
     * @return  string
     */
    protected function _compile ()
    {
        // The URI should be considered literal except for keys and optional parts
        // Escape everything preg_quote would escape except for : ( ) < >
        $regex = preg_replace('#' . Route::REGEX_ESCAPE . '#', '\\\\$0', $this->_uri);
        if (strpos($regex, '(') !== FALSE) {
            // Make optional parts of the URI non-capturing and optional
            $regex = str_replace(array('(' , ')'), array('(?:' , ')?'), $regex);
        }
        // Insert default regex for keys
        $regex = str_replace(array('<' , '>'), array('(?P<' , '>' . Route::REGEX_SEGMENT . ')'), $regex);
        if (! empty($this->_regex)) {
            $search = $replace = array();
            foreach ($this->_regex as $key => $value) {
                $search[] = "<$key>" . Route::REGEX_SEGMENT;
                $replace[] = "<$key>$value";
            }
            // Replace the default regex with the user-specified regex
            $regex = str_replace($search, $replace, $regex);
        }
        return '#^' . $regex . '$#';
    }
} // End Route
